#include "mcp.h"

#include "configfile.h"

/* Yes, I "could have just used XML" as a file format.
 * After carefully weighing the costs and advantages, XML sucks and I hate it.
 */

/*
 * TODO  Write a paragraph or two describing how the configuration files
 *       are processed by handing off processing of each section to a
 *       ConfigProcessor that 'wants it,' and that writing is done by
 *       having each ConfigProcessor write out its section(s).
 *       
 *       I would then write a paragraph describing how the MCP has the list
 *       of ConfigProcessors.
 *
 *       However as the design is still a little dodgy I won't write those
 *       paragraphs in any kind of great detail.
 */

bool ConfigFile::add_processor (ConfigProcessor *new_proc, bool check_for_match)
    {
    if ( !new_proc ) return false;
    if ( check_for_match && !scan_for_label(new_proc->label) ) return false;
    new_proc->cfgfile = this;
    procs.push_back(new_proc);
    return true;
    }

void ConfigFile::delete_processors ()
    {
    proc = 0;
    while ( !procs.empty() )
        { delete procs.back(); procs.pop_back(); }
    }

//  File Reading                                                          >fold>

// returns true if the file was opened or if it does not exist:
// check is_open to see if it was actually opened.
bool ConfigFile::open_for_read ()
    {
    // ASSERT(!fl)
    char buf[PATH_MAX+1];
    snprintf(buf, PATH_MAX, "%s/%s", filepath, filename);
    if ( (fl = fopen(buf, "rt")) != 0 )
        {
        line_number = 0;
        last_write_proc = 0;
        mcp->log_verbose(11, "config opened \"%s\" for reading\n", buf);
        return true;
        }
    else
        {
        if ( errno == ENOENT )
            {
            mcp->log_verbose(5, "config not found at %s\n", buf);
            return true;  // an absent config file is not an error
            }
        else
            {
            // TODO  error msgbox here
            fprintf(stderr, "Config file %s: %s\n", buf, strerror(errno));
            return false;
            }
        }
    return false;
    }

// Note: perform_read helper
bool ConfigFile::pr_check_for_label (char *p)
    {
    if ( *p != '[' ) return false;
    // sync any changes here with scan_for_label
    char *end = strstr(p, "]");  // just ignore anything after a ]
    if ( !end ) return false;
    proc = 0;
    *end = '\0';
    p = trim_spaces(p+1);
    if ( *p == '\0' ) return true;

    list<ConfigProcessor *>::iterator it;
    for ( it = procs.begin(); it != procs.end(); it++ )
        if ( strcasecmp(p, (*it)->label) == 0 )
            {
            proc = *it;
            proc->cfgfile = this;
            mcp->log_verbose(11, "%3d:[%s]\n", line_number, proc->label);
            break;
            }

    if ( !proc )
        mcp->log_verbose(1, "rmc: Unmatched section in config: %s\n", p);
    return true;
    }

void ConfigFile::perform_read ()
    {
    char buf[301],
         *p,
         *key,
         *value;

    if ( !procs.empty() )
        {
        proc = 0;
        list<ConfigProcessor *>::iterator it;
        for ( it = procs.begin(); it != procs.end(); it++ )
            (*it)->reading = false;
        }
    else  // caller may manually set it (until a label..)
        {
        // ASSERT(proc)
        proc->cfgfile = this;
        }

    while ( !feof(fl) )
        {
        if ( fgets(buf, 300, fl) )
            {
            line_number++;
            p = skip_spaces(buf);
            if ( *p == '\0' ||
                 *p == '#' || *p == ';' || strncmp(p, "//", 2) == 0 )
                continue;
            if ( pr_check_for_label(p) )
                continue;
            if ( !proc )
                {
                mcp->log_verbose(11, "skipped: %s\n", p);
                continue;
                }

            value = strstr(p, ":");  // key/data delimiter
            if ( !value && !(value = strstr(p, "=")) )  // eh.. accept = also
                {
                mcp->log_verbose(1, "Config line %d, bad data: %s\n",
                                 line_number, p);
                continue;
                }
            *value = '\0';
            key = trim_spaces(p);
            value = trim_spaces(value + 1);
            if ( *key == '\0' ) continue;
            // blank value string is ok, may be clearing a default value

            mcp->log_verbose(11, "    %3d:%s:%s\n", line_number, key, value);
            if ( !proc->reading )
                {
                proc->init_read();
                proc->reading = true;
                }
            proc->data_read(key, value);
            }
        if ( ferror(fl) )
            {
            mcp->log_verbose(1, "Error reading config file: %d: %s\n",
                             line_number, strerror(errno));
            fclose(fl);
            break;
            }
        }

    list<ConfigProcessor *>::iterator it;
    for ( it = procs.begin(); it != procs.end(); it++ )
        if ( (*it)->reading )
            (*it)->read_done();

    if ( !procs.empty() )
        proc = 0;
    }

bool ConfigFile::scan_for_label (const char *label)
    {
    fpos_t fpos;
    char buf[301],
         *p;
    if ( !label ) label = "";

    fgetpos(fl, &fpos);
    rewind(fl);
    while ( !feof(fl) )
        {
        if ( fgets(buf, 300, fl) )
            {
            p = skip_spaces(buf);
            if ( *p == '[' )  // sync any changes here with pr_check_for_label
                {
                char *end = strstr(p, "]");  // just ignore anything after a ]
                if ( !end ) return false;
                *end = '\0';
                p = trim_spaces(p+1);
                if ( *p == '\0' ) return true;  // blank is still a label

                if ( strcasecmp(p, label) == 0 )
                    {
                    fsetpos(fl, &fpos);
                    return true;
                    }
                }
            }
        }

    fsetpos(fl, &fpos);
    return false;
    }

// ---------------------------------------------------------------------- <fold<
//  File Writing                                                          >fold>

// returns true if a file was opened for writing in the path directory
bool ConfigFile::open_for_write ()
    {
    // ASSERT(!fl && !filepath_write)
    char buf[PATH_MAX+1];
    snprintf(buf, PATH_MAX, "%s/%s.new", filepath, filename);
    if ( (fl = fopen(buf, "wt")) != 0 )
        {
        line_number = 0;
        last_write_proc = 0;
        mcp->log_verbose(11, "config opened \"%s\" for writing\n", buf);
        filepath_write = strdup(buf);
        return true;
        }
    else
        {
        // TODO  error msgbox this
        fprintf(stderr, "Config file %s: %s\n", buf, strerror(errno));
        }
    return false;
    }

void ConfigFile::perform_write ()
    {
    list<ConfigProcessor *>::iterator it;
    for ( it = procs.begin(); it != procs.end(); it++ )
        {
        proc = *it;
        proc->write_data();
        }
    }

void ConfigFile::cancel_write ()
    {
    if ( filepath_write )
        {
        fclose(fl); fl = 0;
        unlink(filepath_write);
        free(filepath_write);
        filepath_write = 0;
        }
    }

// Don't use this unless you are:
// 1)  using write("\n") to add a blank line
// 2)  really, really sure the text you are writing will not
//     interfere with the file format
void ConfigFile::write (const char *str)
    {
    fputs(str, fl);
    }

void ConfigFile::write (const char *key, int minimum_key_width,
                        bool leftjust, const char *value, ...)
    {
    int i;
    if ( proc != last_write_proc )
        write_label();

    for ( i = 0; i < indent; i++ )
        fputc(' ', fl);

    i = minimum_key_width - strlen(key);
    if ( !leftjust )
        for ( ; i > 0; i-- )
            fputc(' ', fl);
    fprintf(fl, "%s ", key);
    if ( leftjust )
        for ( ; i > 0; i-- )
            fputc(' ', fl);
    fputc(':', fl);  // key/data delimiter

    if ( value && *value != '\0' )
        {
        fputc(' ', fl);
        va_list vargs;
        va_start(vargs, value);
        vfprintf(fl, value, vargs);
        va_end(vargs);
        }
    fputc('\n', fl);

    if ( ferror(fl) )
        {
        mcp->log_verbose(1, "Error writing config file: %d: %s\n",
                         line_number+1, strerror(errno));
        return;
        }
    else
        {
        line_number++;
        mcp->log_verbose(11, "    %3d:%s:%s\n", line_number, key, value);
        }
    }

void ConfigFile::write_comment (const char *comment)
    {
// - comments may be written just before the block name
// - though we may get comments written without a block this way.. FIXME
//    if ( proc && proc != last_write_proc )
//        write_label();
    fputc('#', fl); fputc(' ', fl);
    fputs(comment, fl);
    line_number++;
    }

void ConfigFile::write_label ()
    {
    last_write_proc = proc;
    if ( line_number != 0 )
        {
        fputs("", fl);
        line_number++;
        }
    fprintf(fl, "[ %s ]\n", proc->label);
    line_number++;
    }

// ---------------------------------------------------------------------- <fold<
//  File Closing                                                          >fold>

// TODO trim this down a bit.
void ConfigFile::close_file ()
    {
    // ASSERT(fl)
    if ( !filepath_write )  // close a reading file
        {
        fclose(fl);
        }
    else  // close a new config file, then replace the old one
        {
        if ( fclose(fl) == 0 )
            {
            char buf[PATH_MAX+1];
            snprintf(buf, PATH_MAX, "%s/%s", filepath, filename);
#ifdef BROKEN_RENAME
            if ( unlink(buf) == 0 || errno == ENOENT )
                {
#endif
                if ( rename(filepath_write, buf) == 0 )
                    ;  // success!
                else
                    {
                    // unlink(filepath_write);
                    mcp->log_verbose(1, "Error replacing the old config file (%s) with the new one (%s): %s\n"
                                     "Manual intervention possibly needed.\n",
                                     filename, filepath_write, strerror(errno));
                    }
#ifdef BROKEN_RENAME
                }
            else
                mcp->log_verbose(1, "Wrote new config; failed to unlink the old \"%s\": %s\n", buf, strerror(errno));
#endif
            }
        else
            {
            mcp->log_verbose(1, "Writing config file: %s\n", strerror(errno));
            unlink(filepath_write);
            }
        free(filepath_write);
        filepath_write = 0;
        }
    fl = 0;
    }

// ---------------------------------------------------------------------- <fold<

void ConfigProcessor::read_error (const char *key, const char *value,
                                  const char *expected)
    {
    mcp->log_verbose(1, "config file: line %d [%s]: %s (%s)\n",
                     cfgfile->line_number, label, key, value);
    if ( expected )
        mcp->log_verbose(1, "    expected: %s\n", expected);
    }

// TODO:  read_error(key, value, "format", ...)  or something similar
